Note:
# Import required libraries
import torch.nn as nn
import torch
import numpy as np
import matplotlib.pyplot as plt
import math
from torchvision.utils import make_grid
%matplotlib inline
# for auto-reloading external modules
# see http://stackoverflow.com/questions/1907993/autoreload-of-modules-in-ipython
%load_ext autoreload
%autoreload 2
Please read the information below even if you are familiar with GANs. There are some terms below that will be used in the coding part.
Generative models try to model the distribution of the data in an explicit way, in the sense that we can easily sample new data points from this model. This is in contrast to discriminative models that try to infer the output from the input. In class and in the previous problem, we have seen one classic deep generative model, the Variational Autoencoder (VAE). Here, we will learn another generative model that has risen to prominence in recent years, the Generative Adversarial Network (GAN).
As the math of Generative Adversarial Networks are somewhat tedious, a story is often told of a forger and a police officer to illustrate the idea.
Imagine a forger that makes fake bills, and a police officer that tries to find these forgeries. If the forger were a VAE, his goal would be to take some real bills, and try to replicate the real bills as precisely as possible. With GANs, the forger has a different idea: rather than trying to replicate the real bills, it suffices to make fake bills such that people think they are real.
Now let's start. In the beginning, the police knows nothing about how to distinguish between real and fake bills. The forger knows nothing either and only produces white paper.
In the first round, the police gets the fake bill and learns that the forgeries are white while the real bills are green. The forger then finds out that white papers can no longer fool the police and starts to produce green papers.
In the second round, the police learns that real bills have denominations printed on them while the forgeries do not. The forger then finds out that plain papers can no longer fool the police and starts to print numbers on them.
In the third round, the police learns that real bills have watermarks on them while the forgeries do not. The forger then has to reproduce the watermarks on his fake bills.
...
Finally, the police is able to spot the tiniest difference between real and fake bills and the forger has to make perfect replicas of real bills to fool the police.
Now in a GAN, the forger becomes the generator and the police becomes the discriminator. The discriminator is a binary classifier with the two classes being "taken from the real data" ("real") and "generated by the generator" ("fake"). Its objective is to minimize the classification loss. The generator's objective is to generate samples so that the discriminator misclassifies them as real.
Here we have some complications: the goal is not to find one perfect fake sample. Such a sample will not actually fool the discriminator: if the forger makes hundreds of the exact same fake bill, they will all have the same serial number and the police will soon find out that they are fake. Instead, we want the generator to be able to generate a variety of fake samples such that when presented as a distribution alongside the distribution of real samples, these two are indistinguishable by the discriminator.
So how do we generate different samples with a deterministic generator? We provide it with random numbers as input.
Typically, for the discriminator we use binary cross entropy loss with label 1 being real and 0 being fake. For the generator, the input is a random vector drawn from a standard normal distribution. Denote the generator by $G_\phi(z)$, discriminator by $D_\theta (x)$, the distribution of the real samples by $p(x)$, and the input distribution to the generator by $q(z)$. Recall that the binary cross entropy loss with classifier output $y$ and label $\hat{y}$ is
$$L(y, \hat{y}) = -\hat{y} \log y - (1 - \hat{y}) \log (1 - y)$$For the discriminator, the objective is $$\min_{\theta} \mathrm{E}_{x \sim p(x)}[L(D_{\theta}(x), 1)] + \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 0)]$$
For the generator, the objective is $$\max_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 0)]$$
The generator's objective corresponds to maximizing the classification loss of the discriminator on the generated samples. Alternatively, we can minimize the classification loss of the discriminator on the generated samples when labelled as real: $$\min_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 1)]$$
And this is what we will use in our implementation. The strength of the two networks should be balanced, so we train the two networks alternatingly, updating the parameters in both networks once in each iteration.
Correctly filling out __init__: 7 pts
Correctly filling out training loop: 13 pts
We first load the data (CIFAR-10) and define some convenient functions. You can run the cell below to download the dataset to ./data.
# !wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz -P data
# !tar -xzvf data/cifar-10-python.tar.gz --directory data
# !rm data/cifar-10-python.tar.gz
def unpickle(file):
import sys
if sys.version_info.major == 2:
import cPickle
with open(file, 'rb') as fo:
dict = cPickle.load(fo)
return dict['data'], dict['labels']
else:
import pickle
with open(file, 'rb') as fo:
dict = pickle.load(fo, encoding='bytes')
return dict[b'data'], dict[b'labels']
def load_train_data():
X = []
for i in range(5):
X_, _ = unpickle('data/cifar-10-batches-py/data_batch_%d' % (i + 1))
X.append(X_)
X = np.concatenate(X)
X = X.reshape((X.shape[0], 3, 32, 32))
return X
def load_test_data():
X_, _ = unpickle('data/cifar-10-batches-py/test_batch')
X = X_.reshape((X_.shape[0], 3, 32, 32))
return X
def set_seed(seed):
np.random.seed(seed)
torch.manual_seed(seed)
# Load cifar-10 data
train_samples = load_train_data() / 255.0
test_samples= load_test_data() / 255.0
To save you some mundane work, we have defined a discriminator and a generator for you. Look at the code to see what layers are there.
Note: use the advice on that page with caution if you are using GANs for your team project. It is already 4 years old, which is a really long time in deep learning research. It does not reflect the latest results.
class Generator(nn.Module):
def __init__(self, starting_shape):
super(Generator, self).__init__()
self.fc = nn.Linear(starting_shape, 4 * 4 * 128)
self.upsample_and_generate = nn.Sequential(
nn.BatchNorm2d(128),
nn.LeakyReLU(),
nn.ConvTranspose2d(in_channels=128, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.ConvTranspose2d(in_channels=64, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True),
nn.BatchNorm2d(32),
nn.LeakyReLU(),
nn.ConvTranspose2d(in_channels=32, out_channels=3, kernel_size=4, stride=2, padding=1, bias=True),
nn.Sigmoid()
)
def forward(self, input):
transformed_random_noise = self.fc(input)
reshaped_to_image = transformed_random_noise.reshape((-1, 128, 4, 4))
generated_image = self.upsample_and_generate(reshaped_to_image)
return generated_image
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True),
nn.LeakyReLU(),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1, bias=True),
nn.BatchNorm2d(128),
nn.LeakyReLU(),
)
self.fc = nn.Linear(4 * 4 * 128, 1)
def forward(self, input):
downsampled_image = self.downsample(input)
reshaped_for_fc = downsampled_image.reshape((-1, 4 * 4 * 128))
classification_probs = self.fc(reshaped_for_fc)
return classification_probs
# Use this to put tensors on GPU/CPU automatically when defining tensors
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(device)
class DCGAN(nn.Module):
def __init__(self):
super(DCGAN, self).__init__()
self.num_epoch = 25
self.batch_size = 128
self.log_step = 100
self.visualize_step = 2
self.code_size = 64 # size of latent vector (size of generator input)
self.learning_rate = 2e-4
self.vis_learning_rate = 1e-2
# IID N(0, 1) Sample
self.tracked_noise = torch.randn([64, self.code_size], device=device)
self._actmax_label = torch.ones([64, 1], device=device)
################################################################################
# Prob 2-1: Define the generator and discriminator, and loss functions #
# Also, apply the custom weight initialization (see link: #
# https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html) #
################################################################################
# To-Do: Initialize generator and discriminator
# use variable name "self._generator" and "self._discriminator", respectively
# (also move them to torch device for accelerating the training later)
self._generator = Generator(self.code_size).to(device)
self._discriminator = Discriminator().to(device)
# To-Do: Apply weight initialization (first implement the weight initialization
# function below by following the given link)
self._generator.apply(self._weight_initialization)
self._discriminator.apply(self._weight_initialization)
################################################################################
# Prob 2-1: Define the generator and discriminators' optimizers #
# HINT: Use Adam, and the provided momentum values (betas) #
################################################################################
betas = (0.5, 0.999)
# To-Do: Initialize the generator's and discriminator's optimizers
self.gen_optimizer = torch.optim.Adam(self._generator.parameters(), lr=self.learning_rate, betas=betas)
self.dis_optimizer = torch.optim.Adam(self._discriminator.parameters(), lr=self.learning_rate, betas=betas)
# To-Do: Define weight initialization function
# see link: https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
def _weight_initialization(self, m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
# To-Do: Define a general classification loss function (sigmoid followed by binary cross entropy loss)
def _classification_loss(self, output, target):
return nn.BCEWithLogitsLoss()(output, target)
################################################################################
# END OF YOUR CODE #
################################################################################
# Training function
def train(self, train_samples):
num_train = train_samples.shape[0]
step = 0
# smooth the loss curve so that it does not fluctuate too much
smooth_factor = 0.95
plot_dis_s = 0
plot_gen_s = 0
plot_ws = 0
dis_losses = []
gen_losses = []
max_steps = int(self.num_epoch * (num_train // self.batch_size))
fake_label = torch.zeros([self.batch_size, 1], device=device)
real_label = torch.ones([self.batch_size, 1], device=device)
self._generator.train()
self._discriminator.train()
print('Start training ...')
for epoch in range(self.num_epoch):
np.random.shuffle(train_samples)
for i in range(num_train // self.batch_size):
step += 1
batch_samples = train_samples[i * self.batch_size : (i + 1) * self.batch_size]
batch_samples = torch.Tensor(batch_samples).to(device)
################################################################################
# Prob 2-1: Train the discriminator on all real images first #
################################################################################
# To-Do: HINT: Remember to eliminate all discriminator gradients first! (.zero_grad())
self._discriminator.zero_grad()
# To-Do: feed real samples to the discriminator
real_dis_output = self._discriminator(batch_samples)
# To-Do: calculate the discriminator loss for real samples
# use the variable name "real_dis_loss"
real_dis_loss = self._classification_loss(real_dis_output, real_label)
################################################################################
# Prob 2-1: Train the discriminator with an all fake batch #
################################################################################
# To-Do: sample noises from IID Normal(0, 1)^d on the torch device
noise = torch.randn(self.batch_size, self.code_size, device=device)
# To-Do: generate fake samples from the noise using the generator
fake_samples = self._generator(noise).detach()
# To-Do: feed fake samples to discriminator
# Make sure to detach the fake samples from the gradient calculation
# when feeding to the discriminator, we don't want the discriminator to
# receive gradient info from the Generator
fake_dis_output = self._discriminator(fake_samples)
# To-Do: calculate the discriminator loss for fake samples
# use the variable name "fake_dis_loss"
fake_dis_loss = self._classification_loss(fake_dis_output, fake_label)
# To-Do: calculate the total discriminator loss (real loss + fake loss)
dis_loss = real_dis_loss + fake_dis_loss
# To-Do: calculate the gradients for the total discriminator loss
dis_loss.backward()
# To-Do: update the discriminator weights
self.dis_optimizer.step()
################################################################################
# Prob 2-1: Train the generator #
################################################################################
# To-Do: Remember to eliminate all generator gradients first! (.zero_grad())
self._generator.zero_grad()
# To-Do: sample noises from IID Normal(0, 1)^d on the torch device
noise = torch.randn(self.batch_size, self.code_size, device=device)
# To-Do: generate fake samples from the noise using the generator
fake_samples = self._generator(noise)
# To-Do: feed fake samples to the discriminator
# No need to detach from gradient calculation here, we want the
# generator to receive gradient info from the discriminator
# so it can learn better.
gen_dis_output = self._discriminator(fake_samples)
# To-Do: calculate the generator loss
# hint: the goal of the generator is to make the discriminator
# consider the fake samples as real
gen_loss = self._classification_loss(gen_dis_output, real_label)
# To-Do: Calculate the generator loss gradients
gen_loss.backward()
# To-Do: Update the generator weights
self.gen_optimizer.step()
################################################################################
# END OF YOUR CODE #
################################################################################
dis_loss = real_dis_loss + fake_dis_loss
plot_dis_s = plot_dis_s * smooth_factor + dis_loss * (1 - smooth_factor)
plot_gen_s = plot_gen_s * smooth_factor + gen_loss * (1 - smooth_factor)
plot_ws = plot_ws * smooth_factor + (1 - smooth_factor)
dis_losses.append(plot_dis_s / plot_ws)
gen_losses.append(plot_gen_s / plot_ws)
if step % self.log_step == 0:
print('Iteration {0}/{1}: dis loss = {2:.4f}, gen loss = {3:.4f}'.format(step, max_steps, dis_loss, gen_loss))
if epoch % self.visualize_step == 0:
fig = plt.figure(figsize = (8, 8))
ax1 = plt.subplot(111)
ax1.imshow(make_grid(self._generator(self.tracked_noise.detach()).cpu().detach(), padding=1, normalize=True).numpy().transpose((1, 2, 0)))
plt.show()
dis_losses_cpu = [_.cpu().detach() for _ in dis_losses]
plt.plot(dis_losses_cpu)
plt.title('discriminator loss')
plt.xlabel('iterations')
plt.ylabel('loss')
plt.show()
gen_losses_cpu = [_.cpu().detach() for _ in gen_losses]
plt.plot(gen_losses_cpu)
plt.title('generator loss')
plt.xlabel('iterations')
plt.ylabel('loss')
plt.show()
print('... Done!')
#########################################################
# Prob 2-4: Find the reconstruction of a batch of samples
# **skip this part when working on problem 2-1 and come back for problem 2-4
####################################################################
# Prob 2-4: To-Do: Define squared L2-distance function (or Mean-Squared-Error)
# as reconstruction loss
####################################################################
def _reconstruction_loss(self, original, reconstruction):
mse_loss = nn.MSELoss()
loss = mse_loss(original, reconstruction)
return loss
def reconstruct(self, samples):
recon_code = torch.zeros([samples.shape[0], self.code_size], device=device, requires_grad=True)
samples = torch.tensor(samples, device=device, dtype=torch.float32)
# Set the generator to evaluation mode, to make batchnorm stats stay fixed
self._generator.eval()
################################################################################
# Prob 2-4: complete the definition of the optimizer. #
# **skip this part when working on problem 2-1 and come back for problem 2-4 #
################################################################################
# To-Do: define the optimizer
# Hinit: Use self.vis_learning_rate as one of the parameters for Adam optimizer
optimizer = torch.optim.Adam([recon_code], lr=self.vis_learning_rate)
for i in range(500):
################################################################################
# Prob 2-4: Fill in the training loop for reconstruciton #
# **skip this part when working on problem 2-1 and come back for problem 2-4 #
################################################################################
# To-Do: eliminate the gradients
optimizer.zero_grad()
# To-Do: feed the reconstruction codes to the generator for generating reconstructed samples
# use the variable name "recon_samples"
# recon_samples = [] # comment out this line when you are coding
recon_samples = self._generator(recon_code)
# To-Do: calculate reconstruction loss
# use the variable name "recon_loss"
# recon_loss = 0.0 # comment out this line when you are coding
recon_loss = self._reconstruction_loss(samples, recon_samples)
# To-Do: calculate the gradient of the reconstruction loss
recon_loss.backward()
# To-Do: update the weights
optimizer.step()
################################################################################
# END OF YOUR CODE #
################################################################################
return recon_loss, recon_samples.detach().cpu()
# Perform activation maximization on a batch of different initial codes
def actmax(self, actmax_code):
self._generator.eval()
self._discriminator.eval()
################################################################################
# Prob 2-4: just check this function. You do not need to code here #
# skip this part when working on problem 2-1 and come back for problem 2-4 #
################################################################################
actmax_code = torch.tensor(actmax_code, device=device, dtype=torch.float32, requires_grad=True)
actmax_optimizer = torch.optim.Adam([actmax_code], lr=self.vis_learning_rate)
for i in range(500):
actmax_optimizer.zero_grad()
actmax_sample = self._generator(actmax_code)
actmax_dis = self._discriminator(actmax_sample)
actmax_loss = self._classification_loss(actmax_dis, self._actmax_label)
actmax_loss.backward()
actmax_optimizer.step()
return actmax_sample.detach().cpu()
cpu
Now let's do the training!
Don't panic if the loss curve goes wild. The two networks are competing for the loss curve to go different directions, so virtually anything can happen. If your code is correct, the generated samples should have a high variety.
Do NOT change the number of epochs, learning rate, or batch size. If you're using Google Colab, the batch size will not be an issue during training.
set_seed(42)
dcgan = DCGAN()
dcgan.train(train_samples)
torch.save(dcgan.state_dict(), "dcgan.pt")
Start training ... Iteration 100/9750: dis loss = 0.0242, gen loss = 4.7371 Iteration 200/9750: dis loss = 0.1963, gen loss = 4.2365 Iteration 300/9750: dis loss = 0.1831, gen loss = 3.0957
Iteration 400/9750: dis loss = 0.3073, gen loss = 2.5033 Iteration 500/9750: dis loss = 0.7252, gen loss = 1.9279 Iteration 600/9750: dis loss = 0.6413, gen loss = 2.1152 Iteration 700/9750: dis loss = 0.7381, gen loss = 1.2792 Iteration 800/9750: dis loss = 0.6322, gen loss = 1.8668 Iteration 900/9750: dis loss = 0.8249, gen loss = 1.5181 Iteration 1000/9750: dis loss = 0.8457, gen loss = 1.3573 Iteration 1100/9750: dis loss = 0.7046, gen loss = 2.0542
Iteration 1200/9750: dis loss = 0.6909, gen loss = 1.7588 Iteration 1300/9750: dis loss = 0.6322, gen loss = 2.8097 Iteration 1400/9750: dis loss = 1.0496, gen loss = 1.0888 Iteration 1500/9750: dis loss = 1.2120, gen loss = 2.2414 Iteration 1600/9750: dis loss = 0.7388, gen loss = 1.4633 Iteration 1700/9750: dis loss = 1.2186, gen loss = 4.7139 Iteration 1800/9750: dis loss = 0.6511, gen loss = 1.3907 Iteration 1900/9750: dis loss = 0.8867, gen loss = 1.1356
Iteration 2000/9750: dis loss = 0.6245, gen loss = 2.0864 Iteration 2100/9750: dis loss = 0.6485, gen loss = 2.2099 Iteration 2200/9750: dis loss = 0.8509, gen loss = 1.0685 Iteration 2300/9750: dis loss = 0.6576, gen loss = 2.3609 Iteration 2400/9750: dis loss = 0.5921, gen loss = 1.1232 Iteration 2500/9750: dis loss = 0.6502, gen loss = 1.4605 Iteration 2600/9750: dis loss = 0.6544, gen loss = 1.9450 Iteration 2700/9750: dis loss = 0.6432, gen loss = 1.7844
Iteration 2800/9750: dis loss = 0.7671, gen loss = 2.1639 Iteration 2900/9750: dis loss = 0.7231, gen loss = 2.1439 Iteration 3000/9750: dis loss = 0.4755, gen loss = 1.5791 Iteration 3100/9750: dis loss = 0.6820, gen loss = 2.3492 Iteration 3200/9750: dis loss = 0.6313, gen loss = 2.7131 Iteration 3300/9750: dis loss = 1.0817, gen loss = 0.5598 Iteration 3400/9750: dis loss = 0.8664, gen loss = 0.8195 Iteration 3500/9750: dis loss = 0.5589, gen loss = 1.4254
Iteration 3600/9750: dis loss = 1.0048, gen loss = 3.0033 Iteration 3700/9750: dis loss = 0.6441, gen loss = 1.9350 Iteration 3800/9750: dis loss = 0.6687, gen loss = 1.8029 Iteration 3900/9750: dis loss = 0.6432, gen loss = 2.2945 Iteration 4000/9750: dis loss = 0.8771, gen loss = 1.3276 Iteration 4100/9750: dis loss = 0.7499, gen loss = 1.1601 Iteration 4200/9750: dis loss = 0.8214, gen loss = 1.6256
Iteration 4300/9750: dis loss = 0.6122, gen loss = 1.3319 Iteration 4400/9750: dis loss = 0.7781, gen loss = 1.3181 Iteration 4500/9750: dis loss = 0.6645, gen loss = 1.3263 Iteration 4600/9750: dis loss = 0.5925, gen loss = 2.2705 Iteration 4700/9750: dis loss = 0.7422, gen loss = 1.9046 Iteration 4800/9750: dis loss = 0.9329, gen loss = 1.1878 Iteration 4900/9750: dis loss = 0.6349, gen loss = 1.8777 Iteration 5000/9750: dis loss = 0.7414, gen loss = 1.5771
Iteration 5100/9750: dis loss = 1.0029, gen loss = 1.3782 Iteration 5200/9750: dis loss = 0.7696, gen loss = 3.2044 Iteration 5300/9750: dis loss = 0.7195, gen loss = 0.8833 Iteration 5400/9750: dis loss = 0.8012, gen loss = 1.1361 Iteration 5500/9750: dis loss = 0.6462, gen loss = 1.1582 Iteration 5600/9750: dis loss = 0.6338, gen loss = 1.7607 Iteration 5700/9750: dis loss = 0.8577, gen loss = 1.3380 Iteration 5800/9750: dis loss = 0.5926, gen loss = 1.1607
Iteration 5900/9750: dis loss = 0.7722, gen loss = 0.9965 Iteration 6000/9750: dis loss = 0.7009, gen loss = 1.9003 Iteration 6100/9750: dis loss = 0.7345, gen loss = 2.2285 Iteration 6200/9750: dis loss = 0.9916, gen loss = 2.6323 Iteration 6300/9750: dis loss = 0.8046, gen loss = 0.6664 Iteration 6400/9750: dis loss = 0.6828, gen loss = 1.6762 Iteration 6500/9750: dis loss = 0.6719, gen loss = 2.1459 Iteration 6600/9750: dis loss = 0.9165, gen loss = 0.9933
Iteration 6700/9750: dis loss = 0.8604, gen loss = 1.3723 Iteration 6800/9750: dis loss = 0.7405, gen loss = 1.0229 Iteration 6900/9750: dis loss = 0.6814, gen loss = 1.9916 Iteration 7000/9750: dis loss = 0.7313, gen loss = 1.2672 Iteration 7100/9750: dis loss = 0.6307, gen loss = 1.6243 Iteration 7200/9750: dis loss = 0.5868, gen loss = 0.9988 Iteration 7300/9750: dis loss = 0.7052, gen loss = 1.3433 Iteration 7400/9750: dis loss = 0.6030, gen loss = 2.2972
Iteration 7500/9750: dis loss = 0.6712, gen loss = 2.5240 Iteration 7600/9750: dis loss = 0.6221, gen loss = 1.7364 Iteration 7700/9750: dis loss = 0.7302, gen loss = 2.0268 Iteration 7800/9750: dis loss = 0.5484, gen loss = 1.4585 Iteration 7900/9750: dis loss = 0.8659, gen loss = 2.0835 Iteration 8000/9750: dis loss = 0.6011, gen loss = 1.2096 Iteration 8100/9750: dis loss = 0.8000, gen loss = 0.5239
Iteration 8200/9750: dis loss = 0.9386, gen loss = 2.5513 Iteration 8300/9750: dis loss = 0.6625, gen loss = 1.0007 Iteration 8400/9750: dis loss = 0.6534, gen loss = 1.3538 Iteration 8500/9750: dis loss = 0.6206, gen loss = 1.9188 Iteration 8600/9750: dis loss = 0.6297, gen loss = 1.4937 Iteration 8700/9750: dis loss = 0.6720, gen loss = 1.9332 Iteration 8800/9750: dis loss = 0.7953, gen loss = 1.3757 Iteration 8900/9750: dis loss = 1.3104, gen loss = 2.8429
Iteration 9000/9750: dis loss = 0.7458, gen loss = 1.8146 Iteration 9100/9750: dis loss = 0.8443, gen loss = 1.3797 Iteration 9200/9750: dis loss = 0.5252, gen loss = 1.4908 Iteration 9300/9750: dis loss = 0.5969, gen loss = 2.1540 Iteration 9400/9750: dis loss = 0.5416, gen loss = 1.7929 Iteration 9500/9750: dis loss = 0.7410, gen loss = 1.6499 Iteration 9600/9750: dis loss = 0.3404, gen loss = 2.9867 Iteration 9700/9750: dis loss = 0.6182, gen loss = 1.8015
... Done!
Here are two questions related to the use of Batch Normalization in GANs. Q1 below will not be graded and the answer is provided. But you should attempt to solve it before looking at the answer.
Q1: We made separate batches for real samples and fake samples when training the discriminator. Is this just an arbitrary design decision made by the inventor that later becomes the common practice, or is it critical to the correctness of the algorithm? [0 pt]
Answer to Q1: When we are training the generator, the input batch to the discriminator will always consist of only fake samples. If we separate real and fake batches when training the discriminator, then the fake samples are normalized in the same way when we are training the discriminator and when we are training the generator. If we mix real and fake samples in the same batch when training the discriminator, then the fake samples are not normalized in the same way when we train the two networks, which causes the generator to fail to learn the correct distribution.
Q2: Look at the construction of the discriminator carefully. You will find that between dis_conv1 and dis_lrelu1 there is no batch normalization. This is not a mistake. What could go wrong if there were a batch normalization layer there? Why do you think that omitting this batch normalization layer solves the problem practically if not theoretically? [3 pt]
Please provide your answer to Q2: If there was a batch normalization in the first layer, then it might hinder the performance. This might be because if you normalize the input distribution, the difference between the distribution betwewen real and fake data can be partially masked. This might make the discriminiator have a harder time differentiating the real and the fake. By not using the batch normalization, it will make it easier for the discriminator to capture the difference between the real and fake data distribution, improving the performance of the GAN.
Takeaway from this problem: always excercise extreme caution when using batch normalization in your network!
For further info (optional): you can read this paper to find out more about why Batch Normalization might be bad for your GANs: On the Effects of Batch and Weight Normalization in Generative Adversarial Networks
Spectral norm is a way of stabilizing the GAN training of discriminator. Please add the embedded spectral norm function in Pytorch to the Discriminator class below in order to test its effects. (see link: https://pytorch.org/docs/stable/generated/torch.nn.utils.spectral_norm.html)
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
################################################################################
# Prob 2-3: #
# adding spectral norm to the discriminator #
################################################################################
self.downsample = nn.Sequential(
nn.utils.spectral_norm(nn.Conv2d(in_channels=3, out_channels=32, kernel_size=4, stride=2, padding=1, bias=True)),
nn.LeakyReLU(),
nn.utils.spectral_norm(nn.Conv2d(in_channels=32, out_channels=64, kernel_size=4, stride=2, padding=1, bias=True)),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.utils.spectral_norm(nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1, bias=True)),
nn.BatchNorm2d(128),
nn.LeakyReLU(),
)
################################################################################
# END OF YOUR CODE #
################################################################################
self.fc = nn.Linear(4 * 4 * 128, 1)
def forward(self, input):
downsampled_image = self.downsample(input)
reshaped_for_fc = downsampled_image.reshape((-1, 4 * 4 * 128))
classification_probs = self.fc(reshaped_for_fc)
return classification_probs
After adding the spectral norm to the discriminator, redo the training block below to see the effects.
set_seed(42)
dcgan = DCGAN()
dcgan.train(train_samples)
torch.save(dcgan.state_dict(), "dcgan.pt")
Start training ... Iteration 100/9750: dis loss = 0.4008, gen loss = 3.2815 Iteration 200/9750: dis loss = 0.1847, gen loss = 3.2941 Iteration 300/9750: dis loss = 0.2434, gen loss = 3.6901
Iteration 400/9750: dis loss = 0.4914, gen loss = 2.3408 Iteration 500/9750: dis loss = 0.7105, gen loss = 2.6949 Iteration 600/9750: dis loss = 0.4409, gen loss = 2.4475 Iteration 700/9750: dis loss = 0.7734, gen loss = 2.4643 Iteration 800/9750: dis loss = 0.6835, gen loss = 1.4125 Iteration 900/9750: dis loss = 1.0732, gen loss = 2.9692 Iteration 1000/9750: dis loss = 0.7200, gen loss = 1.0311 Iteration 1100/9750: dis loss = 0.6040, gen loss = 1.9684
Iteration 1200/9750: dis loss = 0.6041, gen loss = 2.8225 Iteration 1300/9750: dis loss = 1.2394, gen loss = 1.1113 Iteration 1400/9750: dis loss = 0.5149, gen loss = 2.7565 Iteration 1500/9750: dis loss = 0.4713, gen loss = 2.3292 Iteration 1600/9750: dis loss = 0.5577, gen loss = 2.5701 Iteration 1700/9750: dis loss = 0.5773, gen loss = 1.4931 Iteration 1800/9750: dis loss = 0.9438, gen loss = 2.8252 Iteration 1900/9750: dis loss = 0.6160, gen loss = 1.4538
Iteration 2000/9750: dis loss = 0.7081, gen loss = 1.1883 Iteration 2100/9750: dis loss = 0.6822, gen loss = 2.1588 Iteration 2200/9750: dis loss = 0.6661, gen loss = 1.9622 Iteration 2300/9750: dis loss = 0.6604, gen loss = 2.3691 Iteration 2400/9750: dis loss = 0.5759, gen loss = 2.1471 Iteration 2500/9750: dis loss = 0.7622, gen loss = 1.5490 Iteration 2600/9750: dis loss = 0.5783, gen loss = 1.6578 Iteration 2700/9750: dis loss = 0.7096, gen loss = 1.9638
Iteration 2800/9750: dis loss = 0.6118, gen loss = 1.7022 Iteration 2900/9750: dis loss = 0.8075, gen loss = 1.2900 Iteration 3000/9750: dis loss = 0.9000, gen loss = 0.6559 Iteration 3100/9750: dis loss = 0.7396, gen loss = 3.0561 Iteration 3200/9750: dis loss = 0.8268, gen loss = 1.4785 Iteration 3300/9750: dis loss = 0.7691, gen loss = 2.2451 Iteration 3400/9750: dis loss = 0.7069, gen loss = 1.3209 Iteration 3500/9750: dis loss = 0.8842, gen loss = 2.6923
Iteration 3600/9750: dis loss = 0.5223, gen loss = 1.7201 Iteration 3700/9750: dis loss = 0.8958, gen loss = 1.8229 Iteration 3800/9750: dis loss = 0.6256, gen loss = 1.8334 Iteration 3900/9750: dis loss = 0.6970, gen loss = 1.7520 Iteration 4000/9750: dis loss = 0.6502, gen loss = 1.3285 Iteration 4100/9750: dis loss = 0.6627, gen loss = 1.8326 Iteration 4200/9750: dis loss = 0.5819, gen loss = 2.6059
Iteration 4300/9750: dis loss = 0.6667, gen loss = 2.1297 Iteration 4400/9750: dis loss = 0.6649, gen loss = 2.0389 Iteration 4500/9750: dis loss = 0.6354, gen loss = 2.0256 Iteration 4600/9750: dis loss = 0.6818, gen loss = 1.5043 Iteration 4700/9750: dis loss = 0.9688, gen loss = 0.9026 Iteration 4800/9750: dis loss = 0.7747, gen loss = 1.4131 Iteration 4900/9750: dis loss = 0.5679, gen loss = 1.7282 Iteration 5000/9750: dis loss = 0.6229, gen loss = 1.3746
Iteration 5100/9750: dis loss = 0.7182, gen loss = 2.6245 Iteration 5200/9750: dis loss = 0.5057, gen loss = 1.2609 Iteration 5300/9750: dis loss = 0.6317, gen loss = 1.6988 Iteration 5400/9750: dis loss = 0.6762, gen loss = 1.9397 Iteration 5500/9750: dis loss = 0.6861, gen loss = 1.1922 Iteration 5600/9750: dis loss = 0.8390, gen loss = 2.9640 Iteration 5700/9750: dis loss = 0.7057, gen loss = 1.3042 Iteration 5800/9750: dis loss = 0.6549, gen loss = 2.0386
Iteration 5900/9750: dis loss = 0.4774, gen loss = 1.4654 Iteration 6000/9750: dis loss = 0.6534, gen loss = 1.9180 Iteration 6100/9750: dis loss = 0.8208, gen loss = 1.8732 Iteration 6200/9750: dis loss = 0.5947, gen loss = 1.5159 Iteration 6300/9750: dis loss = 0.6228, gen loss = 2.0762 Iteration 6400/9750: dis loss = 0.5683, gen loss = 1.6108 Iteration 6500/9750: dis loss = 0.5484, gen loss = 2.2216 Iteration 6600/9750: dis loss = 0.7523, gen loss = 1.6811
Iteration 6700/9750: dis loss = 0.6939, gen loss = 2.3649 Iteration 6800/9750: dis loss = 0.7799, gen loss = 1.5089 Iteration 6900/9750: dis loss = 0.5649, gen loss = 1.6141 Iteration 7000/9750: dis loss = 0.6770, gen loss = 1.5322 Iteration 7100/9750: dis loss = 0.6434, gen loss = 2.5814 Iteration 7200/9750: dis loss = 0.6033, gen loss = 1.1357 Iteration 7300/9750: dis loss = 0.5414, gen loss = 2.5772 Iteration 7400/9750: dis loss = 0.6446, gen loss = 2.5061
Iteration 7500/9750: dis loss = 0.6004, gen loss = 2.8679 Iteration 7600/9750: dis loss = 0.5725, gen loss = 2.2049 Iteration 7700/9750: dis loss = 0.5115, gen loss = 1.4609 Iteration 7800/9750: dis loss = 0.8966, gen loss = 0.7793 Iteration 7900/9750: dis loss = 0.6216, gen loss = 1.7505 Iteration 8000/9750: dis loss = 0.4766, gen loss = 2.0940 Iteration 8100/9750: dis loss = 0.4452, gen loss = 2.4923
Iteration 8200/9750: dis loss = 0.8749, gen loss = 1.3514 Iteration 8300/9750: dis loss = 0.6431, gen loss = 1.8592 Iteration 8400/9750: dis loss = 0.6533, gen loss = 1.9075 Iteration 8500/9750: dis loss = 0.5335, gen loss = 1.0163 Iteration 8600/9750: dis loss = 0.3980, gen loss = 1.9965 Iteration 8700/9750: dis loss = 0.6359, gen loss = 0.8813 Iteration 8800/9750: dis loss = 0.5798, gen loss = 2.9754 Iteration 8900/9750: dis loss = 0.4520, gen loss = 1.4412
Iteration 9000/9750: dis loss = 0.4780, gen loss = 1.7296 Iteration 9100/9750: dis loss = 0.7289, gen loss = 1.0264 Iteration 9200/9750: dis loss = 0.5206, gen loss = 2.2639 Iteration 9300/9750: dis loss = 0.4151, gen loss = 1.8191 Iteration 9400/9750: dis loss = 0.3716, gen loss = 1.9435 Iteration 9500/9750: dis loss = 0.6760, gen loss = 1.8032 Iteration 9600/9750: dis loss = 0.7037, gen loss = 0.9099 Iteration 9700/9750: dis loss = 0.4501, gen loss = 2.6468
... Done!
Activation Maximization is a visualization technique to see what a particular neuron has learned, by finding the input that maximizes the activation of that neuron. Here we use methods similar to Synthesizing the preferred inputs for neurons in neural networks via deep generator networks.
In short, what we want to do is to find the samples that the discriminator considers most real, among all possible outputs of the generator, which is to say, we want to find the codes (i.e. a point in the input space of the generator) from which the generated images, if labelled as real, would minimize the classification loss of the discriminator:
$$\min_{z} L(D_{\theta}(G_{\phi}(z)), 1)$$Compare this to the objective when we were training the generator:
$$\min_{\phi} \mathrm{E}_{z \sim q(z)}[L(D_{\theta}(G_{\phi}(z)), 1)]$$The function to minimize is the same, with the difference being that when training the network we fix a set of input data and find the optimal model parameters, while in activation maximization we fix the model parameters and find the optimal input.
So, similar to the training, we use gradient descent to solve for the optimal input. Starting from a random code (latent vector) drawn from a standard normal distribution, we perform a fixed step of Adam optimization algorithm on the code (latent vector).
The batch normalization layers should work in evaluation mode.
We provide the code for this part, as a reference for solving the next part. You may want to go back to the code above and check the actmax function and figure out what it's doing:
set_seed(241)
dcgan = DCGAN()
dcgan.load_state_dict(torch.load("dcgan.pt", map_location=device))
actmax_results = dcgan.actmax(np.random.normal(size=(64, dcgan.code_size)))
fig = plt.figure(figsize = (8, 8))
ax1 = plt.subplot(111)
ax1.imshow(make_grid(actmax_results, padding=1, normalize=True).numpy().transpose((1, 2, 0)))
plt.show()
The output should have less variety than those generated from random code, but look realisitic.
A similar technique can be used to reconstruct a test sample, that is, to find the code that most closely approximates the test sample. To achieve this, we only need to change the loss function from discriminator's loss to the squared L2-distance between the generated image and the target image:
$$\min_{z} \left|\left|G_{\phi}(z)-x\right|\right|_2^2$$This time, we always start from a zero vector.
You need to achieve a reconstruction loss < 0.145. Do NOT modify anything outside of the blocks marked for you to fill in.
dcgan = DCGAN()
dcgan.load_state_dict(torch.load("dcgan.pt", map_location=device))
avg_loss, reconstructions = dcgan.reconstruct(test_samples[0:64])
print('average reconstruction loss = {0:.4f}'.format(avg_loss))
fig = plt.figure(figsize = (8, 8))
ax1 = plt.subplot(111)
ax1.imshow(make_grid(torch.from_numpy(test_samples[0:64]), padding=1).numpy().transpose((1, 2, 0)))
plt.show()
fig = plt.figure(figsize = (8, 8))
ax1 = plt.subplot(111)
ax1.imshow(make_grid(reconstructions, padding=1, normalize=True).numpy().transpose((1, 2, 0)))
plt.show()
average reconstruction loss = 0.0140
See the pinned Piazza post for detailed instruction.